export enum QueueExceptionEnum {
  QUEUE_FULL = 'QueueFull',
  QUEUE_EMPTY = 'QueueEmpty'
}

class QueueFull extends Error {
  constructor(...params) {
    super(...params);
    this.name = QueueExceptionEnum.QUEUE_FULL;
  }
}

class QueueEmpty extends Error {
  constructor(...params) {
    super(...params);
    this.name = QueueExceptionEnum.QUEUE_EMPTY;
  }
}

// type QueueOptions = {
//   name?: string
//   maxSize?: number
// }

/**
 *  Simple async FIFO (First In, First Out) queue implementation
 */
export class Queue<T> {

  private maxSize = 0;

  private _getters = [];
  private _putters = [];
  private _items: T[] = [];

  get currSize() {
    return this._items.length;
  }

  isFull(): boolean {
    if (this.maxSize === 0) {
      return false;
    }
    return this._items.length >= this.maxSize;
  }	

  isEmpty(): boolean {
    return this._items.length === 0;
  }

  private _put(item: T) {
    this._items.unshift(item);
  }

  private _get(): T {
    return this._items.pop();
  }

  private _wakeUp(waiters) {
    if (waiters.length > 0) {
      waiters.pop()();
    }
  }

  putNowait(item: T) {
    if (this.isFull()) {
      throw new QueueFull();
    }
    this._put(item);
    this._wakeUp(this._getters);
  }

  /**
   * If the queue is empty, an QueueEmpty exception is thrown
   *  @throws QueueEmpty
   */
  getNowait(): T {
    if (this.isEmpty()) {
      throw new QueueEmpty();
    }
    const item = this._get();
    this._wakeUp(this._putters);
    return item;
  }

  async put(item: T) {
    if (this.isFull()) {
      await new Promise(r => this._putters.unshift(r));
    }
    this.putNowait(item);
  }

  async get(): Promise<T> {
    if (this.isEmpty()) {
      await new Promise(r => this._getters.unshift(r));
    }
    return this.getNowait();
  }

  remove(t: T): boolean {
    const before = this.currSize;
    this._items = this._items.filter(i => i !== t);
    const after = this.currSize;
    return before > after;
  }
}

// const q = new Queue<string>();

// (async () => {
//   // await q.put('1');
//   // await q.put('2');

//   // setTimeout(() => q.put('blabla'), 3000);

//   // const item1 = await q.get();
//   // const item2 = await q.get();
//   // console.log(item1);
//   // console.log(item2);

//   // try {
//   //   const ii = q.getNowait();
//   //   console.log(ii);
//   // } catch (e) {
//   //   console.log(e.name);
//   // }

//   // const r = await q.get();
//   // console.log(r);

//   // setTimeout(() => q.put('blabla'), 3000);

//   // const r = await q.get();
//   // console.log(r);

//   // q.putNowait('aaa');
//   // q.putNowait('bbb');
//   // q.putNowait('ccc');

//   // const b = q.remove('ccc');
//   // console.log(b);
//   // console.log(q.currSize);


//   // console.log(await q.get());
//   // console.log(await q.get());
// })();